“Flow past a cylinder” example

We consider in this example the simulation of incompressible flow past a circular cylinder at \(Re=200\).

The problem to solve is the following, constituted by the dimensionless penalized Vorticty-Transport Equations, also called the Brinkman Navier-Stokes equations in velocity (\(\boldsymbol u\)) -vorticity (\(\boldsymbol \omega\)) formulation:

\[\frac{\partial{\boldsymbol \omega (\boldsymbol x, t)}}{\partial{t}} + \boldsymbol u(\boldsymbol x,t).\nabla \boldsymbol \omega(\boldsymbol x,t) - \boldsymbol \omega(\boldsymbol x,t).\nabla \boldsymbol u(\boldsymbol x,t) = \dfrac{1}{Re} \Delta \boldsymbol \omega(\boldsymbol x,t) + \nabla \times \left (\lambda \chi (\boldsymbol u_{cyl}(\boldsymbol x,t) - \boldsymbol u(\boldsymbol x,t)) \right)\]
\[\Delta \boldsymbol u(\boldsymbol x,t) = -\nabla \times \boldsymbol \omega (\boldsymbol x, t)\]

The first equation is the momentum equation:

  • \(\boldsymbol \omega(\boldsymbol x,t).\nabla \boldsymbol u(\boldsymbol x,t)\) corresponds to the stretching term (which vanishes in 2D)

  • \(\dfrac{1}{Re} \Delta \boldsymbol \omega(\boldsymbol x,t)\) corresponds to the diffusion term, modelling the diffusion of the vorticity field \(\boldsymbol \omega\). \(Re\) denotes the Reynolds number.

  • \(\nabla \times \left (\lambda \chi (\boldsymbol u_{cyl}(\boldsymbol x,t) - \boldsymbol u(\boldsymbol x,t)) \right)\) is the external force term, called the Brinkman penalization term. It allows to model the presence of the cylinder and its action on the flow field.

    • \(\chi\) denotes the characteristic function, equal to 1 inside the cylinder and 0 in the fluid.

    • The penalization parameter \(\lambda\) allows to prescribe the level of permeability of the immersed body (\(\lambda\) is precisely inversely proportional to its permeability). Here one considers a solid cylinder (i.e. zero peremability), the \(\lambda\) parameter is therefore infinite, and is set in this study to \(\lambda=10^8\).

    • The immersed cylinder is supposed fixed (zero velocity), therefore in this study \(\boldsymbol u_{cyl}(\boldsymbol x,t)=0, \ \ \forall \boldsymbol x \in \Omega, \forall t\in \mathbb{R}^+\)

The second equation of the above system is the Poisson equation allowing to recover the velocity field from the vorticity (it is obtained by combining the incompressibility condition $ \nabla `:nbsphinx-math:cdot :nbsphinx-math:boldsymbol u = 0$ and the vorticity definition :math:boldsymbol omega = nabla times boldsymbol u`).

For computational issues, we consider in this example a “false 3D case” in the sense that we consider only few cells in the spanwise direction (=3rd dimension). The domain \(\Omega\) is of size \(L_x \times L_y \times L_z =[-2.56,2.56] \times [-2.56,2.56] \times [-0.32,0.32]\) with grid resolution \((n_x, n_y, n_z) = (64, 64,8)\) and the cylinder, of diameter \(D=1\), is located at \((x,y,z)=(0,0,0)\). The simulation is performed between \(t=0\) and \(t=2\) with initial and inflow condition:

\[\boldsymbol u_{\infty}(\boldsymbol x,t) = (1,0, 0)\]

Periodic boundary conditions are applied on the domain boundaries, and a vorticity absorption is realized within a sponge zone located at the outlet bewteen \(x=4.8\) and \(x=5.12\). A velocity correction is applied after vorticity absorption and the above inflow condition is prescribed in order to set a uniform flow at the inlet while ensuring a non-zero circulation.

The no-slip boundary conditions at the cylinder surface are prescribed through the Brinkman penalization term.

\(\underline{NB}\) : It is important to note that the mesh resolution is clearly too low, the time step too large and the domain too short in both streamwise and vertical directions to obtain a converged solution. This coarse setup has been chosen for a quick overview of such unsteady problem.

The execution of the proposed example may take around 15 min (choose the option “Run :math:`rightarrow` Run all cells” in the menu)

Initialization

Imports

First import all necessary elements from numpy and hysop

[1]:
import hysop
/opt/conda/lib/python3.12/site-packages/pyopencl/scan.py:1144: UserWarning: pytools.persistent_dict 'pyopencl-generated-scan-kernel-cache-v1': enabling safe_sync as default. This provides strong protection against data loss, but can be unnecessarily expensive for use cases such as caches.Pass 'safe_sync=False' if occasional data loss is tolerable. Pass 'safe_sync=True' to suppress this warning.
  generic_scan_kernel_cache = WriteOncePersistentDict(
[2]:
import os
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

pi = np.pi
cos = np.cos
sin = np.sin
[3]:
from hysop import Box, Simulation, Problem, MPIParams, Field, IOParams
from hysop.defaults import VelocityField, VorticityField, \
    EnstrophyParameter, TimeParameters
from hysop.parameters.tensor_parameter import TensorParameter
from hysop.constants import Implementation, AdvectionCriteria, HYSOP_REAL, \
    StretchingFormulation, StretchingCriteria, Backend
from hysop.operators import Advection, StaticDirectionalStretching, Diffusion, \
    PoissonCurl, AdaptiveTimeStep, HDF_Writer,          \
    Enstrophy, MinMaxFieldStatistics, StrangSplitting,    \
    ParameterPlotter, PenalizeVorticity, FlowRateCorrection, \
    VorticityAbsorption, CustomOperator, DirectionalAdvection, \
    DirectionalStretching
from hysop.numerics.odesolvers.runge_kutta import RK2
from hysop.methods import SpaceDiscretization, Remesh, TimeIntegrator, \
    ComputeGranularity, Interpolation, StrangOrder
from hysop.topology.cartesian_topology import CartesianTopology
from hysop.tools.parameters import CartesianDiscretization

Domain

Let’s define the computational domain and the coefficients/parameters relative to the problem to solve

[4]:
dim = 3
npts = (8, 128, 128)
box = Box(dim=dim, origin=(-0.16, -2.56, -2.56), length=(0.32, 5.12, 5.12))
print(box)
Box::d0 | 3D rectangular box domain:
  *origin:  [-0.16 -2.56 -2.56]
  *max_pos: [0.16 2.56 2.56]
  *length:  [0.32 5.12 5.12]
  *left  boundary conditions: [PERIODIC(0), PERIODIC(0), PERIODIC(0)]
  *right boundary conditions: [PERIODIC(0), PERIODIC(0), PERIODIC(0)]

[5]:
cfl = 0.5
lcfl = 0.3
uinf = 1.0
viscosity = 1.0 / 200 # ( = 1 / Re)
outfreq = 50
dt0 = 0.0125

# ====== Cylinder inside the domain ======
RADIUS = 0.5
pos = [0., 0., 0.]

Fields

Let’s define fields and parameters

[6]:
# Define parameters and field (time, timestep, velocity, vorticity, enstrophy, flowrate)
t, dt = TimeParameters()
velo = VelocityField(domain=box)
vorti = VorticityField(velocity=velo)
cyl = Field(domain=box, name="Cyl", is_vector=False)
wdotw = Field(domain=box, is_vector=False, name="WdotW")
enstrophy = EnstrophyParameter()
flowrate = TensorParameter(name="flowrate", shape=(3, ),
                           initial_value=[0., 0., uinf * box.length[1] * box.length[0]])
[7]:
# ======= Function to compute flowrate
def computeFlowrate(t, flowrate):
    fr = np.zeros_like(flowrate.value)
    fr[-1] = uinf * box.length[1] * box.length[0]
    # Impose a vertical perturbation in the time interval [Tstart,Tstart+1] to trigger the instability
    Tstart = 3.0
    if t() >= Tstart and t() <= Tstart + 1.0:
        fr[1] = sin(pi * (t() - Tstart)) * \
                box.length[1] * box.length[0]
    flowrate.value = fr

Operators

Let’s define the different operators involved in the mathematical problem and the numerical parameters (MPI, …)

[8]:
# Get default MPI Parameters from domain (even for serial jobs)
mpi_params = MPIParams(comm=box.task_comm,
                       task_id=box.current_task())

# Setup usual implementation specific variables
impl = Implementation.PYTHON
extra_op_kwds = {'mpi_params': mpi_params}
implIsFortran = impl is Implementation.FORTRAN
backend = Backend.HOST
if (impl is Implementation.PYTHON or implIsFortran):
    method = {}
else:
    msg = f'Unknown implementation or change implementation\'{impl}\'.'
    raise ValueError(msg)
[9]:
# Build the directional operators
# > Directional advection
if implIsFortran:
    advec = Advection(
        implementation=impl,
        name='advec',
        velocity=velo,
        advected_fields=(vorti,),
        variables={velo: npts, vorti: npts},
        dt=dt, **extra_op_kwds)
else:
    advec_dir = DirectionalAdvection(
        implementation=impl,
        name='advec',
        velocity=velo,
        advected_fields=(vorti, ),
        velocity_cfl=0.5,
        variables={velo: npts, vorti: npts},
        dt=dt, **extra_op_kwds)
# > Directional stretching + diffusion
if impl is Implementation.OPENCL:
    StretchOp = DirectionalStretching
else:
    StretchOp = StaticDirectionalStretching
stretch = StretchOp(
    implementation=Implementation.PYTHON if implIsFortran else impl,
    name='stretch',
    formulation=StretchingFormulation.CONSERVATIVE,
    velocity=velo,
    vorticity=vorti,
    variables={velo: npts, vorti: npts},
    dt=dt, **extra_op_kwds)

# > Directional splitting operator subgraph
splitting = StrangSplitting(splitting_dim=dim,
                            order=StrangOrder.STRANG_FIRST_ORDER)
if not implIsFortran:
    splitting.push_operators(advec_dir)
splitting.push_operators(stretch)

Note that the above cell defines the advec and stretch operators. Here, the advection and stretching equations are solved in a single direction in space. We need to specify how to combine all space directions, as an operator splitting. The particle remeshing is included in each splitting step, directionaly.

[10]:
# > Penalization operator
penal = PenalizeVorticity(
    implementation=Implementation.PYTHON,
    name='penalization',
    velocity=velo, vorticity=vorti,
    variables={velo: npts, vorti: npts, cyl: npts},
    obstacles=[cyl, ], coeff=1e8,
    dt=dt, **extra_op_kwds)
[11]:
# > Diffusion operator
diffuse = Diffusion(
    implementation=impl,
    name='diffuse',
    nu=viscosity,
    Fin=vorti,
    variables={vorti: npts},
    dt=dt, **extra_op_kwds)
[12]:
# > Vorticity absorption operator
absorption = VorticityAbsorption(
    implementation=Implementation.PYTHON,
    velocity=velo, vorticity=vorti,
    start_coord=4.8,
    flowrate=flowrate,
    name="absorption",
    variables={velo: npts, vorti: npts},
    dt=dt, **extra_op_kwds)
[13]:
# > Poisson operator to recover the velocity from the vorticity
poisson = PoissonCurl(
    implementation=impl,
    name='poisson',
    velocity=velo,
    vorticity=vorti,
    variables={velo: npts, vorti: npts},
    projection=None,
    **extra_op_kwds)
[14]:
# > Flowrate correction operator to adjust velocity with prescribed flowrate
computeFlowrate = CustomOperator(func=computeFlowrate,
                                 invars=(t, ),
                                 outvars=(flowrate, ))
correctFlowrate = FlowRateCorrection(
    implementation=Implementation.PYTHON,
    name="flowrate_correction",
    velocity=velo, vorticity=vorti,
    flowrate=flowrate,
    dt=dt,
    variables={velo: npts, vorti: npts},
    **extra_op_kwds)
[15]:
# > Operator to compute the enstrophy
enstrophy_op = Enstrophy(
    name='enstrophy',
    vorticity=vorti, enstrophy=enstrophy, WdotW=wdotw,
    variables={vorti: npts, wdotw: npts},
    implementation=Implementation.PYTHON if implIsFortran else impl, **extra_op_kwds)
[16]:
# > outputs operator : write 3D velocity and vorticity fields in h5 format every required frequency
io_params = IOParams(filename='fields', frequency=outfreq)
dump_fields = HDF_Writer(name='fields', io_params=io_params,
                         force_backend=backend,
                         variables={velo: npts, vorti: npts}, **extra_op_kwds)
[17]:
# > Operator to compute the infinite norm of the velocity
min_max_U = MinMaxFieldStatistics(name='min_max_U', field=velo,
                                  Finf=True, implementation=Implementation.PYTHON if implIsFortran else impl, variables={velo: npts},
                                  **extra_op_kwds)
# > Operator to compute the infinite norm of the vorticity
min_max_W = MinMaxFieldStatistics(name='min_max_W', field=vorti,
                                  Finf=True, implementation=Implementation.PYTHON if implIsFortran else impl, variables={vorti: npts},
                                  **extra_op_kwds)
[18]:
# > Adaptive timestep operator
adapt_dt = AdaptiveTimeStep(dt, equivalent_CFL=True,
                            start_time=10*dt0)  # start adapting timestep at t=10*dt0
dt_cfl = adapt_dt.push_cfl_criteria(cfl=cfl,
                                    Finf=min_max_U.Finf,
                                    equivalent_CFL=True)
dt_advec = adapt_dt.push_advection_criteria(lcfl=lcfl,
                                            Finf=min_max_W.Finf,
                                            criteria=AdvectionCriteria.W_INF)

Problem

Gather all operators in a single problem to solve.

[19]:
# Create the problem we want to solve and insert our
# directional splitting subgraph and the standard operators.
# The method dictionnary passed to this graph will be dispatched
# accross all operators contained in the graph.
method = {}
method.update({SpaceDiscretization:   4,
              TimeIntegrator:        RK2,
              Remesh:                Remesh.L4_2,
              Interpolation:         Interpolation.LINEAR})
problem = Problem(method=method)
problem.insert(
    computeFlowrate,
    penal,
    splitting,
    diffuse)
if implIsFortran:
    problem.insert(advec)
problem.insert(
    absorption,
    poisson,
    correctFlowrate,
    enstrophy_op, min_max_U, min_max_W, dump_fields,
    adapt_dt
)
[19]:
Problem::node3

Build all lower level stuff

Tell hysop to perform all lover level initialization (allocations, discretization of operators, numerical methods setup, …)

[20]:
problem.build()
<string>:11: RuntimeWarning: invalid value encountered in cast

Setup time discretization

Enforce a time stepping from the prescribed CFL and the velocity field.

[21]:
# Create a simulation
# (do not forget to specify the t and dt parameters here)
simu = Simulation(start=0.0, end=2.0,
                  max_iter=500,
                  dt0=0.0125,
                  t=t, dt=dt)
simu.write_parameters(t, dt_cfl, dt_advec, dt, enstrophy, flowrate,
                      min_max_U.Finf, min_max_W.Finf, adapt_dt.equivalent_CFL,
                      filename='parameters.txt', precision=8)

Initial conditions

Setup the initial conditions for this example in velocity, vorticity and cylinder definition.

[22]:
# ======= Function to compute the cylinder field  =======
def computeCylinder(data, coords, component):
    assert (component == 0)
    (x, y, z) = coords
    dx = x[0, 0, 1] - x[0, 0, 0]
    dy = y[0, 1, 0] - y[0, 0, 0]
    dz = z[1, 0, 0] - z[0, 0, 0]
    data[...] = 0.
    def chi(x, y, z): return np.sqrt((x-pos[0])*(x-pos[0])+(y-pos[1])*(y-pos[1])+(z-z)) <= RADIUS
    data[chi(x, y, z)] = 1.

# ======= Function to compute initial velocity  =======
def computeVel(data, coords, component):
    data[...] = uinf if (component == 0) else 0.0

# ======= Function to compute initial vorticity =======
def computeVort(data, coords, component):
    data[...] = 0.0

problem.initialize_field(cyl, formula=computeCylinder)
problem.initialize_field(velo, formula=computeVel)
problem.initialize_field(vorti, formula=computeVort)

Solving the problem

Perform the time steps computations, i.e. repeat the operators listed in problem through the time steps.

[23]:
# Finally solve the problem
problem.solve(simu)

Post-process the solution

Here we aim at ploting the snapshots of velocity or vorticity magnitude in the \(XY\) plane at different times and at tracing the enstrophy evolution until \(t_{end}=2\):

[24]:
import h5py
[25]:
def extract_field(file, resols, lengths, origins, field_to_plot):
    '''
    file : complete name of the file to read
    resols : array containing grid resolution in each direction (z,y,x)
    lengths : array containing the domain length in each direction (z,y,x)
    origins : array containing the domain origin in each direction (z,y,x)
    field_to_plot : choose between 'Velocity' or 'Vorticity'
    '''

    nx, ny, nz = resols[2], resols[1], resols[0]
    Lx, Ly, Lz = lengths[2], lengths[1], lengths[0]
    Ox, Oy, Oz = origins[2], origins[1], origins[0]
    X, Y, Z = np.arange(nx) * (Lx / nx) + Ox, \
              np.arange(ny) * (Ly / ny) + Oy, \
              np.arange(nz) * (Lz / nz) + Oz

    x0 = np.where(X==0)[0]
    y0 = np.where(Y==0)[0]
    z0 = np.where(Z==0)[0]
    with h5py.File(file, 'r') as f:
        if field_to_plot=='Vorticity' :
            ux = np.array(f['W0_t1'])[:,:,:]
            uy = np.array(f['W1_t1'])[:,:,:]
            uz = np.array(f['W2_t1'])[:,:,:]
        # otherwise, consider the velocity field
        elif field_to_plot=='Velocity' :
            ux = np.array(f['UX_t1'])[:,:,:]
            uy = np.array(f['UY_t1'])[:,:,:]
            uz = np.array(f['UZ_t1'])[:,:,:]
        else :
            msg = f'Non available field'
            raise ValueError(msg)
    Umxy = np.squeeze(np.sqrt(ux[z0, :, :] ** 2 + uy[z0, :, :] ** 2 + uz[z0, :, :] ** 2))
    Umxz = np.squeeze(np.sqrt(ux[:, y0, :] ** 2 + uy[:, y0, :] ** 2 + uz[:, y0, :] ** 2))
    return X, Y, Umxy, Umxz

def plot_field(x, y, u):
    plt.pcolormesh(x, y, u, vmin=0., vmax=1.4, cmap='GnBu', shading='gouraud')
    plt.contour(x, y, u, levels=np.linspace(0., 1.4, 20), linewidths=0.75, colors='k')
    plt.gca().add_artist(plt.Circle((0.,0.), 0.5, fill=False, color='r', ls='-', lw=2))
[26]:
resols = [8, 128, 128]
lengths = [0.32, 5.12, 5.12]
origins = [-0.16, -2.56, -2.56]
files=['interactive/fields_000000.h5',
       'interactive/fields_000050.h5',
       'interactive/fields_000100.h5',
       'interactive/fields_000150.h5']
field_to_plot = 'Vorticity' # possibility to change with 'Velocity'

X,Y,Umxy1,Umxz1 = extract_field(files[0], resols, lengths, origins, field_to_plot)
X,Y,Umxy2,Umxz2 = extract_field(files[1], resols, lengths, origins, field_to_plot)
X,Y,Umxy3,Umxz3 = extract_field(files[2], resols, lengths, origins, field_to_plot)
X,Y,Umxy4,Umxz4 = extract_field(files[3], resols, lengths, origins, field_to_plot)
[27]:
fig=plt.figure(figsize=(8,8))
plt.title(str(field_to_plot)+' magnitude',fontsize=12)
fig.add_subplot(221)

plot_field(X,Y,Umxy1)
plt.xlim(origins[2], lengths[2] + origins[2])
plt.ylim(origins[1], lengths[1] + origins[1])
plt.ylabel('y/D',fontsize=12)
plt.title('t=0',fontsize=12)

fig.add_subplot(222)
plot_field(X,Y,Umxy2)
plt.xlim(origins[2], lengths[2] + origins[2])
plt.ylim(origins[1], lengths[1] + origins[1])
plt.title('t=0.51',fontsize=12)

fig.add_subplot(223)
plot_field(X,Y,Umxy3)
plt.xlim(origins[2], lengths[2] + origins[2])
plt.ylim(origins[1], lengths[1] + origins[1])
plt.xlabel('x/D',fontsize=12)
plt.ylabel('y/D',fontsize=12)
plt.title('t=1.05',fontsize=12)

fig.add_subplot(224)
plot_field(X,Y,Umxy4)
plt.xlim(origins[2], lengths[2] + origins[2])
plt.ylim(origins[1], lengths[1] + origins[1])
plt.xlabel('x/D',fontsize=12)
plt.title('t=1.63',fontsize=12)

plt.show()
../_images/examples_Cylinder_38_0.png
[28]:
data = np.loadtxt('interactive/parameters.txt', skiprows=2)
t = data[:,0]
enstrophy = data[:,4]

fig=plt.figure(figsize=(4,4))
plt.plot(t,enstrophy)
plt.xlabel('t',fontsize=10)
plt.ylabel('enstrophy',fontsize=10)
plt.grid(True)
../_images/examples_Cylinder_39_0.png

Clean up memory

[ ]:
# Finalize
problem.finalize()
[ ]: